/*
 * ModelCC, distributed under ModelCC Shared Software License, www.modelcc.org
 */

package org.modelcc.language.syntax;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.modelcc.io.java.Reflection;
import org.modelcc.language.metamodel.MemberCollection;
import org.modelcc.language.metamodel.CompositeLanguageElement;
import org.modelcc.language.metamodel.LanguageModel;
import org.modelcc.language.metamodel.LanguageElement;
import org.modelcc.language.metamodel.LanguageMember;

/**
 * Object Wrapper.
 * 
 * @author Luis Quesada (lquesada@modelcc.org) & Fernando Berzal (fberzal@modelcc.org)
 */
public class ObjectWrapper implements Serializable 
{    
    /**
     * Wrapped object.
     */
    private Object o;
    
    /**
     * Track used for basic elements.
     */
    private Object track;
    
    /**
     * The model.
     */
    private LanguageModel m;
    
    /**
     * Key
     */
    private boolean key;
    
    /**
     * Hash code
     */
    private int hash;
    
    /**
     * The map.
     */
    private Map<Object,ObjectWrapper> mymap;


    /**
     * Constructor
     * @param o the object
     * @param m the model.
     * @param track used for basic elements. 
     */
    public ObjectWrapper(Object o, LanguageModel m, Object track) 
    {
        this.o = o;
        this.m = m;
        this.track = track;
        this.hash = track.hashCode();
        this.mymap = new HashMap<Object,ObjectWrapper>();
        this.mymap.put(o, (this));
    }
    
    /**
     * Constructor
     * @param o the object
     * @param m the model.
     * @param hash the hash code.
     * @param mymap my mappings. 
     */
    protected ObjectWrapper (Object o, LanguageModel m, int hash, Map<Object,ObjectWrapper> mymap, boolean key) 
    {
        this.o = o;
        this.m = m;
        this.hash = hash;
        this.key = key;
        this.mymap = mymap;
    }


    /**
     * Element members
     * @param cme Composite element
     * @param key is key?
     * @return key members when key is true, all members when key is false
     */
    private static List<LanguageMember> getMembers (CompositeLanguageElement cme, boolean key)
    {
    	if (key)
    		return cme.getKeyMembers();
		else
			return cme.getMembers();
    }
    
    /**
     * Gets a ObjectWrapper
     * @param o the object to wrap
     * @param m the model
     * @param map the object wrappers map.
     * @return the object wrapper.
     */
    public static ObjectWrapper createObjectWrapper(Object o,LanguageModel m,Map<Object,ObjectWrapper> map) 
    {
        return createObjectWrapper(o,m,map,null,false);
    }

    /**
	 * Gets a KeyWrapper
	 * @param o the object to wrap
	 * @param m the model
	 * @param map the object wrappers map.
	 * @return the key wrapper.
	 */
	public static ObjectWrapper createKeyWrapper (Object o,LanguageModel m,Map<Object,ObjectWrapper> map) 
	{
		return createObjectWrapper(o,m,map,null,true);
	} 
    
    /**
     * Gets a ObjectWrapper
     * @param o the object to wrap
     * @param m the model
     * @param map the object wrappers map.
     * @param added last added mappings.
     * @return the object wrapper.
     */
    private static ObjectWrapper createObjectWrapper (
    		Object o,
    		LanguageModel m,
    		Map<Object,ObjectWrapper> map,
    		Map<Object,ObjectWrapper> added, 
    		boolean key ) 
    {
        int hash = 0;
        
        ObjectWrapper kw = map.get(o);
        if (kw != null) {
            if (added != null) {
                added.putAll(kw.mymap);
            }
            return kw;
        }
        
        LanguageElement e = m.getClassToElement().get(o.getClass());
    	Map<Object,ObjectWrapper> myadded = new HashMap<Object,ObjectWrapper>();
        
        if (e instanceof CompositeLanguageElement) {
        	hash = hashComposite(o, m, map, key, myadded, (CompositeLanguageElement) e);
        } else {
        	hash = o.hashCode();        	
        }
        //System.err.println("LE "+e +"OBJ "+o+" "+o.getClass()+" hash="+hash);
    	        
        ObjectWrapper th = new ObjectWrapper(o, m, hash, myadded, key);

        map.put(o,th);
        if (added != null)
            added.putAll(myadded);
        
        return th;
    }

	private static int hashComposite(
			Object o, LanguageModel m,
			Map<Object, ObjectWrapper> map, boolean key,
			Map<Object, ObjectWrapper> myadded, CompositeLanguageElement me) 
	{
		int hash = 0;
		
		try {
			for (LanguageMember em: getMembers(me,key) ) {
				Field fld = Reflection.findField(me.getElementClass(),em.getID());
				fld.setAccessible(true);
				Object val = fld.get(o);
				if (val == null) {
					hash *= 53;
				} else {
					if (MemberCollection.class.isAssignableFrom(em.getClass())) {
						MemberCollection mem = (MemberCollection) em;
						switch (mem.getCollection()) {
						case ARRAY:
							Object[] array = (Object[])val;
							for (int j = 0;j < array.length;j++) {
								ObjectWrapper ow = createObjectWrapper(array[j],m,map,myadded,false);
								map.put(array[j],ow);
								hash += ow.hashCode();
								hash *= 53;
							}
							break;
						case LIST:
							List list = (List)val;
							for (int j = 0;j < list.size();j++) {
								ObjectWrapper ow = createObjectWrapper(list.get(j),m,map,myadded,false);
								map.put(list.get(j),ow);
								hash += ow.hashCode();
								hash *= 53;
							}
							break;
						case SET:
							Set<ObjectWrapper> dictionary = new HashSet<ObjectWrapper>();
							Set<Object> set = (Set<Object>)val;
							for (Object on: set) {
								ObjectWrapper ow = createObjectWrapper(on,m,map,myadded,false);
								map.put(on,ow);
								dictionary.add(ow);
							}
							for (ObjectWrapper w: dictionary) {
								hash += w.hashCode();
							}
							hash *= 53;
							break;
						}
					} else {
						ObjectWrapper ow = createObjectWrapper(val,m,map,myadded,key);
						map.put(val,ow);
						hash += val.toString().hashCode(); // vs. ow.hashCode();
						hash *= 53;
					}
				}
			}
		} catch (Exception ex) {
			Logger.getLogger(ObjectWrapper.class.getName()).log(Level.SEVERE, null, ex);
		}
		return hash;
	}
    
    
    // equals

    @Override
    public boolean equals(Object obj) 
    {
        if (obj == null) 
            return false;

        if (getClass() != obj.getClass()) 
            return false;

        ObjectWrapper other = (ObjectWrapper) obj;

        if (key != other.key)
        	return false;
        
        if (hashCode()!=other.hashCode()) 
            return false;
        
        LanguageElement me = m.getClassToElement().get(o.getClass());

        if (CompositeLanguageElement.class.isAssignableFrom(me.getClass())) {
            return equalsComposite(other);         
        } else {
            return track.equals(other.track);
        }
    }

    
	private boolean equalsComposite(ObjectWrapper other) 
	{
		CompositeLanguageElement cme = (CompositeLanguageElement) m.getClassToElement().get(o.getClass());
	
		try {
			
			for (LanguageMember em: getMembers(cme,key) ) {
				
		        Field fld = Reflection.findField(cme.getElementClass(),em.getID());
		        fld.setAccessible(true);
		        
		        Object val = fld.get(o);
		        Object val2 = fld.get(other.o);
		        
		        Map<Object,ObjectWrapper> mymap2 = other.mymap;
		        
		        if (val == null && val2 == null) {
		        } else if (val != null && val2 == null) {
		            return false;
		        } else if (val == null && val2 != null) {
		            return false;
		        } else {
		        	if (MemberCollection.class.isAssignableFrom(em.getClass())) {
		        		MemberCollection mem = (MemberCollection) em;
		        		switch (mem.getCollection()) {
		        		case ARRAY:
		        			Object[] array = (Object[])val;
		        			Object[] array2 = (Object[])val2;
		        			if (array.length!=array2.length)
		        				return false;
		        			for (int j=0; j<array.length;j++) {
		        				if (!createObjectWrapper(array[j],m,mymap).equals(createObjectWrapper(array2[j],m,mymap2)))
		        					return false;
		        			}
		        			break;
		        		case LIST:
		        			List list = (List)val;
		        			List list2 = (List)val2;
		        			if (list.size()!=list2.size())
		        				return false;
		        			for (int j = 0;j < list.size();j++) {
		        				if (!createObjectWrapper(list.get(j), m,mymap).equals(createObjectWrapper(list2.get(j), m,mymap2)))
		        					return false;
		        			}
		        			break;
		        		case SET:
		        			Set<Object> set = (Set<Object>)val;
		        			Set<Object> set2 = (Set<Object>)val2;
		        			Set<ObjectWrapper> listo = new HashSet<ObjectWrapper>();
		        			Set<ObjectWrapper> listo2 = new HashSet<ObjectWrapper>();
		        			for (Iterator ite = set.iterator();ite.hasNext();) {
		        				listo.add(createObjectWrapper(ite.next(), m,mymap));
		        			}
		        			for (Iterator ite = set2.iterator();ite.hasNext();) {
		        				listo2.add(createObjectWrapper(ite.next(), m,mymap2));
		        			}
		        			if (listo.size()!=listo2.size())
		        				return false;
		        			for (Iterator ite = listo.iterator();ite.hasNext();) {
		        				if (!listo2.contains(ite.next()))
		        					return false;
		        			}
		        			break;
		        		}
		            } else {
		            	return val.toString().equals(val2.toString());
		            }
		        }
		    }
		} catch (Exception ex) {
		    Logger.getLogger(ObjectWrapper.class.getName()).log(Level.SEVERE, null, ex);
		}
		
		return true;
	}
	

    @Override
    public final int hashCode() 
    {
        return hash;
    }
    
    
    @Override
    public String toString ()
    {
    	return "Wrapper for "+(key?"KEY ":"")+o.getClass()+": "+o+" ("+track+") hash="+hash;
    }

}
